Skip to content

feat(ai): AI gateway integration with custom provider support #1072

Open
ddecrulle wants to merge 5 commits into
mainfrom
ia-integration
Open

feat(ai): AI gateway integration with custom provider support #1072
ddecrulle wants to merge 5 commits into
mainfrom
ia-integration

Conversation

@ddecrulle

@ddecrulle ddecrulle commented May 5, 2026

Copy link
Copy Markdown
Member
  • AI gateway port & adapter — new Ai port (OIDC token exchange, model listing) with an OpenWebUI adapter and a mock; config comes from the deployment region
    (region.ai)
  • ai usecase — initialize lifecycle (pending/success/no-account/error), token refresh, model selection; selected model is injected into Helm values when launching a
    service
  • "AI" tab in account settings — displays gateway credentials (API base URL, token, model selector), manages custom AI providers (OpenAI-compatible endpoints) stored in
    localStorage
  • "Test connection" button — validates a custom provider before saving (fetches /models), shows success with model count or an error message
  • Simplified provider card — no longer displays apiBase/apiKey after saving, only the provider name and model selector
  • i18n — 9 languages (en, fr, de, es, fi, it, nl, no, zh-CN)
image

Summary by CodeRabbit

  • New Features
    • Added AI Gateway credentials tab in account settings for configuring API access and managing authentication tokens.
    • Support for region-based AI provider configurations with OAuth authentication.
    • Ability to add, edit, and delete custom OpenAI-compatible AI provider configurations.
    • Model and embeddings model selection interface for AI providers.
    • Multi-language UI support for AI gateway management.

@sonarqubecloud

sonarqubecloud Bot commented May 5, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds full AI gateway integration to Onyxia: a new Ai port type, OpenWebUI adapter performing OIDC token exchange, provider-centric Redux state machine with region and custom providers, bootstrap wiring that creates memoized OIDC instances per AI config, an AccountAiGatewayTab UI component, and AccountAiGatewayTab translations across nine languages.

Changes

AI Gateway Integration

Layer / File(s) Summary
Developer guide
web/CLAUDE.md
Adds repository guidance for Claude Code covering commands, architecture, patterns, and key library roles.
Port types and contracts
web/src/core/ports/Ai.ts, web/src/core/ports/OnyxiaApi/DeploymentRegion.ts, web/src/core/ports/OnyxiaApi/XOnyxia.ts, web/src/core/adapters/onyxiaApi/ApiTypes.ts
Ai port gains id/name and typed listModels; DeploymentRegion gains ai[]; XOnyxiaContext gains optional ai config; ApiTypes gains optional ai in /public/configuration services data.
OIDC token exchange utility
web/src/core/tools/oidcTokenExchange.ts
Adds OidcTokenExchangeError storing HTTP status and oidcTokenExchange() that POSTs OIDC tokens and extracts the returned bearer token.
AI adapters
web/src/core/adapters/ai/openWebUi.ts, web/src/core/adapters/ai/mock.ts, web/src/core/adapters/ai/index.ts
OpenWebUI adapter implements OIDC exchange and Zod-validated model listing; mock adapter matches new id/name signature; barrel export added.
Persisted AI config
web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.ts
Defines PersistedAiConfig types, Zod schema, parseAiConfigStr with null/JSON/schema guards, and serializeAiConfig.
AI state machine
web/src/core/usecases/ai/state.ts
Provider-centric state with Provider.Region/Provider.Custom, State.NotInitialized/Initialized, ModelCatalog lifecycle, and reducers for token refresh, model selection, and custom provider CRUD.
AI selectors and thunks
web/src/core/usecases/ai/selectors.ts, web/src/core/usecases/ai/thunks.ts, web/src/core/usecases/ai/index.ts
selectors.main/activeProvider project state into XOnyxiaContext; thunks handle availability, token refresh, model selection, custom provider CRUD/test, and protectedThunks.initialize building all providers from region config and persisted state.
Bootstrap and wiring
web/src/core/bootstrap.ts, web/src/core/adapters/onyxiaApi/onyxiaApi.ts, web/src/core/usecases/index.ts, web/src/core/usecases/launcher/thunks.ts, web/src/core/usecases/userConfigs.ts
UserConfigs gains aiConfigStr; onyxiaApi maps DeploymentRegion.ai; bootstrap adds init_ai block creating memoized OIDC instances and dispatching initialize; ai usecase registered; launcher injects ai into XOnyxiaContext.
Account AI tab
web/src/ui/pages/account/AccountAiTab.tsx, web/src/ui/pages/account/Page.tsx, web/src/ui/pages/account/accountTabIds.ts, web/src/ui/i18n/types.ts
AccountAiGatewayTab renders region/custom provider cards with credential fields, model selection, and add/edit dialog with async test/save flows; registered as lazy tab in Page.tsx; ai added to tab id union and ComponentKey.
Internationalization (9 languages)
web/src/ui/i18n/resources/*.tsx
Adds Account.ai label and AccountAiGatewayTab translation blocks with credential labels, provider management strings, and no-account guidance across de, en, es, fi, fr, it, nl, no, zh-CN.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A bunny hops through ports and adapters so fine,
Exchanging OIDC tokens along the vine 🐇
New providers appear — region or custom made,
The AI tab glows in every language displayed!
State machines hum, the models cascade down,
The gateway opens wide beneath the cloud's crown ☁️✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ai): AI gateway integration with custom provider support' accurately and clearly describes the main feature being added: AI gateway integration with support for custom AI providers. It is specific, concise, and directly related to the core changes in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ia-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
web/src/core/ports/Ai.ts (1)

8-11: ⚡ Quick win

error variant carries no diagnostic payload — error context is lost

The { status: "error" } branch gives thunks and UI no information about what went wrong (network failure, unexpected HTTP status, etc.), making it hard to display a meaningful error message. Even a minimal reason?: string or httpStatus?: number would allow the UI to differentiate transient failures from configuration errors.

♻️ Proposed extension
 export type GetTokenResult =
     | { status: "success"; token: string }
     | { status: "no-account" }
-    | { status: "error" };
+    | { status: "error"; reason?: string };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/ports/Ai.ts` around lines 8 - 11, The GetTokenResult union
currently has an { status: "error" } variant with no diagnostic data; update the
type so the "error" branch carries minimal payload (e.g., reason?: string and
httpStatus?: number or similar fields) so callers can surface meaningful
messages; change the union definition for GetTokenResult to include those
optional fields and then update all places that construct or pattern-match on
GetTokenResult (e.g., any code creating a { status: "error" } result or
switching on GetTokenResult.status) to populate and consume the new fields.
web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)

196-203: 💤 Low value

enabled: true discriminant is redundant given | undefined

Since ai is typed as { enabled: true; ... } | undefined, the enabled: true literal adds no information — an AI block being present already implies it is enabled. Chart templates that check ai.enabled could equivalently check ai != null. Consider dropping the enabled field to keep the shape consistent with how vault and s3 are modelled in the same type.

♻️ Proposed simplification
     ai:
         | {
-              enabled: true;
               token: string;
               apiBase: string;
               model: string;
           }
         | undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 196 - 203, Remove the
redundant enabled: true literal from the ai union type in XOnyxia (i.e., change
the ai type from "{ enabled: true; token: string; apiBase: string; model:
string; } | undefined" to "{ token: string; apiBase: string; model: string; } |
undefined") and update any runtime checks that test ai.enabled to instead test
for ai != null (or Boolean(ai)), ensuring places referencing the removed enabled
property (e.g., code paths that read ai.enabled) are adjusted to treat
presence/absence of the ai object as the enablement signal.
web/src/core/adapters/ai/openWebUi.ts (1)

40-43: ⚡ Quick win

Unguarded cast on data.data may throw a TypeError at runtime.

If the gateway returns a response where data.data is undefined, null, or not an array, .map() will throw, which callers don't expect — listModels should either validate or provide a fallback.

🛡️ Proposed defensive guard
 const data = await response.json();

-return (data.data as { id: string }[]).map(m => m.id);
+const entries: unknown[] = Array.isArray(data.data) ? data.data : [];
+return (entries as { id: string }[]).map(m => m.id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/adapters/ai/openWebUi.ts` around lines 40 - 43, The unguarded
cast in listModels reads response.json() into data and assumes data.data is an
array, which can throw when it's undefined/null/not-an-array; update the
listModels implementation to validate that data && Array.isArray(data.data)
before mapping, and if not return an empty array (or a safe fallback). When
mapping, defensively extract ids (e.g., filter items with an id property and
coerce to string) so the method always returns string[] without throwing.
web/src/core/usecases/ai/thunks.ts (1)

145-150: 🏗️ Heavy lift

testCustomProvider returns a Promise<string[]> which violates the reactive thunk pattern.

Per the coding guidelines for web/src/core/**/*.ts: "Use observable state with thunks (reactive pattern) rather than promise-based returns; dispatch actions and read state instead of returning values from thunks."

The test result (model list or error) should be stored in state via new actions (e.g., providerTestStarted, providerTestSucceeded({ models }), providerTestFailed) and exposed via a selector, with the UI subscribing to state instead of awaiting the thunk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/usecases/ai/thunks.ts` around lines 145 - 150,
testCustomProvider currently returns a Promise<string[]> which breaks the
project's reactive thunk pattern; change it to dispatch actions and update state
instead of returning values. Replace the direct return of fetchModels(apiBase,
apiKey) in testCustomProvider with dispatch(providerTestStarted()), call
fetchModels, then on success dispatch(providerTestSucceeded({ models })) and on
error dispatch(providerTestFailed({ error })); update the reducer/state to store
test status and models and expose them via a selector for the UI to subscribe
to, and make the thunk return void (or Promise<void>) rather than
Promise<string[]>; reference the existing testCustomProvider thunk and
fetchModels helper when applying these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/core/usecases/ai/selectors.ts`:
- Around line 16-26: Replace the hard assertion in the "no-account" branch so
the selector won't throw when region.ai is missing: remove the assert on
region.ai in the branch where initializationStatus === "no-account" and return
isEnabled: false and initializationStatus as before but only attach webUiUrl
when region.ai exists (e.g., use a safe optional access or conditional property)
so the selector returns a safe fallback instead of throwing; look for symbols
initializationStatus, region.ai and the returned webUiUrl in this function to
update.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 190-193: The call to ai.listModels(token) can throw and leaves the
initializeStart/“pending” state unresolved; wrap the ai.listModels(token)
invocation in a try/catch around the block that follows initializeStart and, on
any error, dispatch initializeFailed with the error (or a normalized message) so
the thunk resolves correctly; reference tokenResult, ai.listModels(token),
initializeStart, and initializeFailed when locating where to add the try/catch
and the error dispatch.

In `@web/src/core/usecases/launcher/thunks.ts`:
- Around line 777-790: The current ai block builds an ai object even when
aiState.selectedModel is undefined (using selectedModel ?? ""), which injects an
empty model into the XOnyxiaContext; update the anonymous IIFE in the ai
property (the code using aiUsecase.selectors.main(getState()) and aiState) to
return undefined unless aiState.isEnabled, aiState.token are present AND
aiState.selectedModel is a non-empty value (e.g., check aiState.selectedModel !=
null && aiState.selectedModel !== ""), otherwise return undefined so no ai key
is written into the context.

In `@web/src/ui/i18n/resources/no.tsx`:
- Around line 126-127: Update the localized helper string for the translation
key "custom providers section helper" to include the user-facing disclosure that
credentials (API keys) are stored locally in the browser; locate the string for
"custom providers section helper" in web/src/ui/i18n/resources/no.tsx and modify
its value to append a short sentence making the storage disclosure (e.g.,
"API-nøkler lagres i nettleseren"), keeping the rest of the guidance about base
URL and API key unchanged.

In `@web/src/ui/pages/account/AccountAiTab.tsx`:
- Around line 100-112: Wrap the ai.addCustomProvider call inside onSaveProvider
in a try/catch so failures are handled: call ai.addCustomProvider(...) in the
try block and only clear the form (setAddFormOpen(false), setPendingLabel(""),
setPendingApiBase(""), setPendingApiKey(""), setTestStatus("idle"),
setTestModelCount(0)) on success; in catch set a failure state (e.g.,
setTestStatus("error") and optionally set an error message state) and keep the
add form open so the user can correct inputs; ensure the catch re-enables any UI
affordances disabled during save and does not leave the promise rejection
unhandled.

In `@web/src/ui/pages/account/Page.tsx`:
- Line 56: The ternary in the .filter callback for accountTabId is redundant;
replace the ternary that returns true when accountTabId !== "ai" else
ai.isAvailable() with a logical OR of the inequality check and ai.isAvailable()
(i.e., test whether accountTabId is not "ai" OR ai.isAvailable()) in the Page
component where accountTabId and ai.isAvailable() are used, and apply the same
simplification to the sibling .filter calls that use the same pattern.

---

Nitpick comments:
In `@web/src/core/adapters/ai/openWebUi.ts`:
- Around line 40-43: The unguarded cast in listModels reads response.json() into
data and assumes data.data is an array, which can throw when it's
undefined/null/not-an-array; update the listModels implementation to validate
that data && Array.isArray(data.data) before mapping, and if not return an empty
array (or a safe fallback). When mapping, defensively extract ids (e.g., filter
items with an id property and coerce to string) so the method always returns
string[] without throwing.

In `@web/src/core/ports/Ai.ts`:
- Around line 8-11: The GetTokenResult union currently has an { status: "error"
} variant with no diagnostic data; update the type so the "error" branch carries
minimal payload (e.g., reason?: string and httpStatus?: number or similar
fields) so callers can surface meaningful messages; change the union definition
for GetTokenResult to include those optional fields and then update all places
that construct or pattern-match on GetTokenResult (e.g., any code creating a {
status: "error" } result or switching on GetTokenResult.status) to populate and
consume the new fields.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 196-203: Remove the redundant enabled: true literal from the ai
union type in XOnyxia (i.e., change the ai type from "{ enabled: true; token:
string; apiBase: string; model: string; } | undefined" to "{ token: string;
apiBase: string; model: string; } | undefined") and update any runtime checks
that test ai.enabled to instead test for ai != null (or Boolean(ai)), ensuring
places referencing the removed enabled property (e.g., code paths that read
ai.enabled) are adjusted to treat presence/absence of the ai object as the
enablement signal.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 145-150: testCustomProvider currently returns a Promise<string[]>
which breaks the project's reactive thunk pattern; change it to dispatch actions
and update state instead of returning values. Replace the direct return of
fetchModels(apiBase, apiKey) in testCustomProvider with
dispatch(providerTestStarted()), call fetchModels, then on success
dispatch(providerTestSucceeded({ models })) and on error
dispatch(providerTestFailed({ error })); update the reducer/state to store test
status and models and expose them via a selector for the UI to subscribe to, and
make the thunk return void (or Promise<void>) rather than Promise<string[]>;
reference the existing testCustomProvider thunk and fetchModels helper when
applying these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2cb9147a-b5ad-46ea-be5c-b123ddd634a2

📥 Commits

Reviewing files that changed from the base of the PR and between d092838 and 38ba553.

📒 Files selected for processing (30)
  • web/CLAUDE.md
  • web/src/core/adapters/ai/index.ts
  • web/src/core/adapters/ai/mock.ts
  • web/src/core/adapters/ai/openWebUi.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/core/adapters/onyxiaApi/onyxiaApi.ts
  • web/src/core/bootstrap.ts
  • web/src/core/ports/Ai.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/ports/OnyxiaApi/XOnyxia.ts
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/usecases/ai/index.ts
  • web/src/core/usecases/ai/selectors.ts
  • web/src/core/usecases/ai/state.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/core/usecases/index.ts
  • web/src/core/usecases/launcher/thunks.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/it.tsx
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/pages/account/AccountAiTab.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/pages/account/accountTabIds.ts

Comment thread web/src/core/usecases/ai/selectors.ts Outdated
Comment thread web/src/core/usecases/ai/thunks.ts Outdated
Comment thread web/src/core/usecases/launcher/thunks.ts Outdated
Comment thread web/src/ui/i18n/resources/no.tsx Outdated
Comment on lines +126 to +127
"custom providers section helper":
"Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep API key storage disclosure in localized helper text.

This helper omits that credentials are stored in the browser, which is important user-facing security context.

Suggested wording patch
-        "custom providers section helper":
-            "Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
+        "custom providers section helper":
+            "Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"custom providers section helper":
"Legg til dine egne AI-leverandører med en basis-URL og API-nøkkel.",
"custom providers section helper":
"Legg til dine egne AI-leverandører (OpenAI, Anthropic eller andre OpenAI-kompatible endepunkter). Legitimasjon lagres i nettleseren din.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/ui/i18n/resources/no.tsx` around lines 126 - 127, Update the
localized helper string for the translation key "custom providers section
helper" to include the user-facing disclosure that credentials (API keys) are
stored locally in the browser; locate the string for "custom providers section
helper" in web/src/ui/i18n/resources/no.tsx and modify its value to append a
short sentence making the storage disclosure (e.g., "API-nøkler lagres i
nettleseren"), keeping the rest of the guidance about base URL and API key
unchanged.

Comment on lines +100 to +112
const onSaveProvider = useConstCallback(async () => {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle add-provider failures in onSaveProvider.

If ai.addCustomProvider fails at Line 101, the rejection is unhandled and the UI gives no clear recovery path. Catch the error and keep the form in a controlled error state.

Proposed fix
     const onSaveProvider = useConstCallback(async () => {
-        await ai.addCustomProvider({
-            label: pendingLabel,
-            apiBase: pendingApiBase,
-            apiKey: pendingApiKey
-        });
+        try {
+            await ai.addCustomProvider({
+                label: pendingLabel,
+                apiBase: pendingApiBase,
+                apiKey: pendingApiKey
+            });
+        } catch {
+            setTestStatus("error");
+            return;
+        }
         setAddFormOpen(false);
         setPendingLabel("");
         setPendingApiBase("");
         setPendingApiKey("");
         setTestStatus("idle");
         setTestModelCount(0);
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onSaveProvider = useConstCallback(async () => {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});
const onSaveProvider = useConstCallback(async () => {
try {
await ai.addCustomProvider({
label: pendingLabel,
apiBase: pendingApiBase,
apiKey: pendingApiKey
});
} catch {
setTestStatus("error");
return;
}
setAddFormOpen(false);
setPendingLabel("");
setPendingApiBase("");
setPendingApiKey("");
setTestStatus("idle");
setTestModelCount(0);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/ui/pages/account/AccountAiTab.tsx` around lines 100 - 112, Wrap the
ai.addCustomProvider call inside onSaveProvider in a try/catch so failures are
handled: call ai.addCustomProvider(...) in the try block and only clear the form
(setAddFormOpen(false), setPendingLabel(""), setPendingApiBase(""),
setPendingApiKey(""), setTestStatus("idle"), setTestModelCount(0)) on success;
in catch set a failure state (e.g., setTestStatus("error") and optionally set an
error message state) and keep the add form open so the user can correct inputs;
ensure the catch re-enables any UI affordances disabled during save and does not
leave the promise rejection unhandled.

.filter(accountTabId =>
accountTabId !== "vault" ? true : vaultCredentials.isAvailable()
)
.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Simplify the negated ternary condition (SonarCloud warning).

accountTabId !== "ai" ? true : ai.isAvailable() reads as a double-negation and SonarCloud flags it. The direct form is clearer:

♻️ Proposed simplification
-.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))
+.filter(accountTabId => accountTabId !== "ai" || ai.isAvailable())

Note: the same pattern appears in the sibling .filter calls on lines 46, 49–52, and 53–55 — those could be cleaned up consistently in a follow-up.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.filter(accountTabId => (accountTabId !== "ai" ? true : ai.isAvailable()))
.filter(accountTabId => accountTabId !== "ai" || ai.isAvailable())
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 56-56: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia&issues=AZ33YH7OM9gut30udPw5&open=AZ33YH7OM9gut30udPw5&pullRequest=1072

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/ui/pages/account/Page.tsx` at line 56, The ternary in the .filter
callback for accountTabId is redundant; replace the ternary that returns true
when accountTabId !== "ai" else ai.isAvailable() with a logical OR of the
inequality check and ai.isAvailable() (i.e., test whether accountTabId is not
"ai" OR ai.isAvailable()) in the Page component where accountTabId and
ai.isAvailable() are used, and apply the same simplification to the sibling
.filter calls that use the same pattern.

ddecrulle and others added 3 commits June 9, 2026 14:09
- Add AI usecase (state/thunks/selectors) with initializeStart/initializeSucceed/initializeFailed lifecycle actions
- getToken() returns a discriminated result type in the Ai port — no-account (403) vs error cases handled without leaking adapter details into usecases
- Gracefully disable AI features on init failure; show a link to the gateway URL when user has no account
- Add AccountAiGatewayTab with token/model display and full i18n for all 9 languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
web/src/core/ports/OnyxiaApi/XOnyxia.ts (1)

185-194: ⚡ Quick win

Prefer optional property syntax for ai context.

ai: {...} | undefined forces callers to always include the key. If this field is truly optional, ai?: {...} is the safer contract for existing XOnyxiaContext object construction paths.

Suggested change
-    ai:
-        | {
+    ai?:
+        {
               enabled: boolean;
               apiKey: string;
               apiBase: string;
               model: string;
               provider: string;
               embeddingsModel: string;
           }
-        | undefined;
+        ;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts` around lines 185 - 194, The ai
property in the XOnyxiaContext type uses union syntax with undefined (ai: {...}
| undefined) which requires callers to always provide the key. Replace this with
the optional property syntax (ai?: {...}) to allow the property to be omitted
entirely when constructing XOnyxiaContext objects, making it a safer contract
for existing code paths that build this type.
web/src/core/ports/Ai.ts (1)

1-8: ⚡ Quick win

Use an interface for the port contract in src/core/ports.

This contract should be declared as an interface to match the ports-and-adapters rule for web/src/core/ports/**/*.ts.

Suggested change
-export type Ai = {
+export interface Ai {
     id: string;
     name: string;
     webUiUrl: string;
     apiBase: string;
     getToken: () => Promise<GetTokenResult>;
     listModels: (token: string) => Promise<{ id: string; name: string }[]>;
-};
+}

As per coding guidelines, web/src/core/ports/**/*.ts: “Define TypeScript interfaces for external dependencies in src/core/ports/ using the ports-and-adapters pattern.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/ports/Ai.ts` around lines 1 - 8, The Ai export in the file
should be declared as an interface instead of a type alias to follow the
ports-and-adapters coding guidelines. Replace the existing type Ai declaration
(which uses the type keyword with an object literal syntax) with an interface
declaration that has the same name and properties, removing the equals sign and
curly brace syntax used for type aliases.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/core/adapters/ai/mock.ts`:
- Line 10: The apiBase string composition concatenates webUiUrl directly with
`/api`, which creates a double slash (`...//api`) if webUiUrl ends with a
forward slash, potentially breaking strict gateways or proxies. Normalize the
webUiUrl by removing any trailing slash before composing the apiBase string. Use
a method to strip the trailing slash from webUiUrl (such as using replace or
trimEnd with proper regex/string manipulation) so that the final apiBase always
has exactly one slash between the base URL and the `/api` path.

In `@web/src/core/usecases/ai/thunks.ts`:
- Around line 31-42: The Zod schema in the fetchModels function requires a name
field that does not exist in the actual OpenAI API response, causing parse
failures. Modify the schema object definition to either make the name field
optional by adding .optional() to the z.string() for name, or use
.default(model.id) to derive the name from the id field as a fallback. If using
the optional approach, update the mapping logic to handle cases where name may
be undefined. If using the default approach, ensure the default value is applied
appropriately in the schema definition.

In `@web/src/core/usecases/launcher/thunks.ts`:
- Line 772: The activeProvider selector can return a valid AI context object
with model set to an empty string when no model is selected, which causes issues
with AI-enabled Helm charts. At the location where
aiUsecase.selectors.activeProvider is called in the launcher thunks, either add
validation logic in the launcher UI to prevent service launch when modelId is
undefined (enforcing model selection before launch), or modify the
activeProvider selector itself to return undefined instead of a valid context
object when provider.selection.modelId is not set, ensuring downstream code
receives undefined rather than a context with an empty model string.

---

Nitpick comments:
In `@web/src/core/ports/Ai.ts`:
- Around line 1-8: The Ai export in the file should be declared as an interface
instead of a type alias to follow the ports-and-adapters coding guidelines.
Replace the existing type Ai declaration (which uses the type keyword with an
object literal syntax) with an interface declaration that has the same name and
properties, removing the equals sign and curly brace syntax used for type
aliases.

In `@web/src/core/ports/OnyxiaApi/XOnyxia.ts`:
- Around line 185-194: The ai property in the XOnyxiaContext type uses union
syntax with undefined (ai: {...} | undefined) which requires callers to always
provide the key. Replace this with the optional property syntax (ai?: {...}) to
allow the property to be omitted entirely when constructing XOnyxiaContext
objects, making it a safer contract for existing code paths that build this
type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 374834a3-0605-4579-b5f9-554819d05232

📥 Commits

Reviewing files that changed from the base of the PR and between 38ba553 and 246bd75.

📒 Files selected for processing (32)
  • web/CLAUDE.md
  • web/src/core/adapters/ai/index.ts
  • web/src/core/adapters/ai/mock.ts
  • web/src/core/adapters/ai/openWebUi.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/core/adapters/onyxiaApi/onyxiaApi.ts
  • web/src/core/bootstrap.ts
  • web/src/core/ports/Ai.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/ports/OnyxiaApi/XOnyxia.ts
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/usecases/ai/decoupledLogic/persistedAiConfig.ts
  • web/src/core/usecases/ai/index.ts
  • web/src/core/usecases/ai/selectors.ts
  • web/src/core/usecases/ai/state.ts
  • web/src/core/usecases/ai/thunks.ts
  • web/src/core/usecases/index.ts
  • web/src/core/usecases/launcher/thunks.ts
  • web/src/core/usecases/userConfigs.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/it.tsx
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/pages/account/AccountAiTab.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/pages/account/accountTabIds.ts
💤 Files with no reviewable changes (13)
  • web/src/ui/i18n/resources/en.tsx
  • web/src/ui/pages/account/accountTabIds.ts
  • web/src/ui/i18n/resources/fr.tsx
  • web/src/ui/i18n/resources/zh-CN.tsx
  • web/src/ui/i18n/types.ts
  • web/src/ui/i18n/resources/it.tsx
  • web/src/core/usecases/ai/index.ts
  • web/src/ui/i18n/resources/nl.tsx
  • web/src/ui/i18n/resources/no.tsx
  • web/src/ui/i18n/resources/es.tsx
  • web/src/ui/pages/account/Page.tsx
  • web/src/ui/i18n/resources/fi.tsx
  • web/src/ui/pages/account/AccountAiTab.tsx
✅ Files skipped from review due to trivial changes (1)
  • web/CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • web/src/core/adapters/ai/index.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/usecases/index.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/ui/i18n/resources/de.tsx
  • web/src/core/tools/oidcTokenExchange.ts
  • web/src/core/adapters/ai/openWebUi.ts

id,
name,
webUiUrl,
apiBase: `${webUiUrl}/api`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize webUiUrl before composing apiBase.

If webUiUrl ends with /, this produces a double slash (...//api), which can break strict gateways/proxies.

Suggested change
 export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai {
     const { id, name, webUiUrl } = params;
+    const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, "");

     return {
         id,
         name,
         webUiUrl,
-        apiBase: `${webUiUrl}/api`,
+        apiBase: `${normalizedWebUiUrl}/api`,
         getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }),
         listModels: async () => [
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
apiBase: `${webUiUrl}/api`,
export function createAi(params: { id: string; name: string; webUiUrl: string }): Ai {
const { id, name, webUiUrl } = params;
const normalizedWebUiUrl = webUiUrl.replace(/\/+$/, "");
return {
id,
name,
webUiUrl,
apiBase: `${normalizedWebUiUrl}/api`,
getToken: async () => ({ status: "success" as const, token: "mock-ai-token" }),
listModels: async () => [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/adapters/ai/mock.ts` at line 10, The apiBase string composition
concatenates webUiUrl directly with `/api`, which creates a double slash
(`...//api`) if webUiUrl ends with a forward slash, potentially breaking strict
gateways or proxies. Normalize the webUiUrl by removing any trailing slash
before composing the apiBase string. Use a method to strip the trailing slash
from webUiUrl (such as using replace or trimEnd with proper regex/string
manipulation) so that the final apiBase always has exactly one slash between the
base URL and the `/api` path.

Comment on lines +31 to +42
async function fetchModels(apiBase: string, apiKey: string): Promise<AiModel[]> {
const response = await fetch(`${apiBase}/models`, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!response.ok) {
throw new Error(`Failed to fetch models (${response.status})`);
}
const { data } = z
.object({ data: z.array(z.object({ id: z.string(), name: z.string() })) })
.parse(await response.json());
return data.map(({ id, name }) => ({ id, name }));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OpenAI API /models endpoint response schema

💡 Result:

The OpenAI API GET /models endpoint returns a list of available models. Its response follows a standard schema containing a top-level object and an array of model entities [1][2]. Response Schema: { "object": "list", "data": [ { "id": "model-id", "object": "model", "created": 1686935002, "owned_by": "openai" } ] } Key Fields: 1. object (string): The type of the top-level object, which is always "list" [1][2]. 2. data (array): A list of model objects [1][2]. Model Object Fields: * id (string): The unique identifier for the model, used in other API endpoints [1][2][3]. * object (string): The type of the object, which is always "model" [1][2][4]. * created (integer): The Unix timestamp (in seconds) representing when the model was created [1][2][3]. * owned_by (string): The name of the organization that owns the model (e.g., "openai") [1][2][3].

Citations:


Fix Zod schema to match actual OpenAI API response format.

The schema at lines 31-42 requires a name field on each model object, but the OpenAI API /models endpoint response includes only id, object, created, and owned_by. The required name field will cause .parse() to fail on real API calls. Either:

  • Make name optional with .optional() and handle the missing field in the mapping, or
  • Derive name from id as a fallback (e.g., .default(model.id))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/usecases/ai/thunks.ts` around lines 31 - 42, The Zod schema in
the fetchModels function requires a name field that does not exist in the actual
OpenAI API response, causing parse failures. Modify the schema object definition
to either make the name field optional by adding .optional() to the z.string()
for name, or use .default(model.id) to derive the name from the id field as a
fallback. If using the optional approach, update the mapping logic to handle
cases where name may be undefined. If using the default approach, ensure the
default value is applied appropriately in the schema definition.

useCertManager: region.certManager?.useCertManager,
certManagerClusterIssuer: region.certManager?.certManagerClusterIssuer
},
ai: aiUsecase.selectors.activeProvider(getState()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the UI enforces model selection before allowing service launch
rg -n "model.*select|selectedModel|modelId" --type=tsx web/src/ui/pages/account/

Repository: InseeFrLab/onyxia

Length of output: 89


🏁 Script executed:

#!/bin/bash
# First, locate the AI usecase files
fd "ai" web/src/core/usecases/ --type f -name "*.ts" | head -20

Repository: InseeFrLab/onyxia

Length of output: 292


🏁 Script executed:

#!/bin/bash
# Search for model selection in UI files with correct syntax
rg -n "model.*select|selectedModel|modelId" web/src/ui/ -A 2 -B 2 | head -100

Repository: InseeFrLab/onyxia

Length of output: 3540


🏁 Script executed:

#!/bin/bash
# Check the AI usecase selectors.ts to understand activeProvider implementation
fd -i "selectors.ts" web/src/core/usecases/ai/

Repository: InseeFrLab/onyxia

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Read the AI usecase selectors to understand activeProvider
cat web/src/core/usecases/ai/selectors.ts

Repository: InseeFrLab/onyxia

Length of output: 2076


🏁 Script executed:

#!/bin/bash
# Check the launcher thunks around line 772 to see context usage
sed -n '760,780p' web/src/core/usecases/launcher/thunks.ts

Repository: InseeFrLab/onyxia

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Check launcher UI for model selection validation
rg "model|selectedModel|activeProvider" web/src/ui/pages/LaunchPad/ -A 2 -B 2 | head -80

Repository: InseeFrLab/onyxia

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Check if there's validation in launcher thunks before using context
sed -n '1,50p' web/src/core/usecases/launcher/thunks.ts | grep -A 5 -B 5 "activeProvider\|ai\."

Repository: InseeFrLab/onyxia

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Find launcher-related UI files
fd -i "launch" web/src/ui/ --type f

Repository: InseeFrLab/onyxia

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Check if any validation happens in launcher thunks for model selection
rg "model|ai\." web/src/core/usecases/launcher/thunks.ts | head -30

Repository: InseeFrLab/onyxia

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check launcher main card for model selection
cat web/src/ui/pages/launcher/LauncherMainCard.tsx

Repository: InseeFrLab/onyxia

Length of output: 22161


🏁 Script executed:

#!/bin/bash
# Search for where ai context is used, specifically model field
rg "\.model|ai\.model" web/src/ --type ts --type tsx -B 2 -A 2 | head -100

Repository: InseeFrLab/onyxia

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Search for where XOnyxiaContext is used, particularly in Helm rendering
rg "XOnyxiaContext" web/src/ -A 3 -B 1 | head -60

Repository: InseeFrLab/onyxia

Length of output: 4380


🏁 Script executed:

#!/bin/bash
# Look for Helm template context or variable injection related to ai.model
rg "ai\\.model|aiUsecase" web/src/core/usecases/launcher/ -A 2 -B 2

Repository: InseeFrLab/onyxia

Length of output: 1108


🏁 Script executed:

#!/bin/bash
# Check if Helm schema validation would catch an empty model
rg "ai\\.model|model.*required|required.*model" web/src/core/usecases/launcher/ -A 2 -B 2

Repository: InseeFrLab/onyxia

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look for XOnyxiaContext type definition to understand ai field structure
fd "OnyxiaApi" web/src/core/ports/ --type f

Repository: InseeFrLab/onyxia

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Read OnyxiaApi to understand XOnyxiaContext and ai field structure
cat web/src/core/ports/OnyxiaApi/OnyxiaApi.ts | head -150

Repository: InseeFrLab/onyxia

Length of output: 3152


🏁 Script executed:

#!/bin/bash
# Search for XOnyxiaContext type definition
rg "type XOnyxiaContext|interface XOnyxiaContext" web/src/core/ports/ -A 30

Repository: InseeFrLab/onyxia

Length of output: 2300


🏁 Script executed:

#!/bin/bash
# Get more of the XOnyxiaContext type definition, particularly the ai field
rg "type XOnyxiaContext" web/src/core/ports/OnyxiaApi/XOnyxia.ts -A 100 | grep -A 50 "ai:"

Repository: InseeFrLab/onyxia

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Read the full XOnyxia.ts file to see the ai field definition
cat web/src/core/ports/OnyxiaApi/XOnyxia.ts | tail -200

Repository: InseeFrLab/onyxia

Length of output: 6364


🏁 Script executed:

#!/bin/bash
# Check if launcher prevents launching without required fields or validates ai.model
rg "ai\.model|model.*required|required.*model|validate.*ai|getXOnyxiaContext" web/src/core/usecases/launcher/thunks.ts -B 3 -A 3 | head -80

Repository: InseeFrLab/onyxia

Length of output: 1134


🏁 Script executed:

#!/bin/bash
# Check if there's any Helm schema validation for model field in launcher
rg "helmValuesSchema|JSONSchema|required|validation" web/src/core/usecases/launcher/decoupledLogic.ts | head -30

Repository: InseeFrLab/onyxia

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Find Helm value computation logic
fd "decoupled\|compute" web/src/core/usecases/launcher/ --type f

Repository: InseeFrLab/onyxia

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the full getXOnyxiaContext function to see if model validation happens
sed -n '690,800p' web/src/core/usecases/launcher/thunks.ts

Repository: InseeFrLab/onyxia

Length of output: 4925


activeProvider selector can return model: "" when no model is selected.

The aiUsecase.selectors.activeProvider returns a valid AI context even when provider.selection.modelId is undefined (defaulting to empty string). The launcher UI does not enforce model selection before launch, so Helm templates can receive ai.model: "", which may cause issues with AI-enabled charts that expect a non-empty model value.

Consider either:

  1. Enforcing model selection in the launcher UI before allowing service launch, or
  2. Adjusting the selector to return undefined when modelId is not set
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/core/usecases/launcher/thunks.ts` at line 772, The activeProvider
selector can return a valid AI context object with model set to an empty string
when no model is selected, which causes issues with AI-enabled Helm charts. At
the location where aiUsecase.selectors.activeProvider is called in the launcher
thunks, either add validation logic in the launcher UI to prevent service launch
when modelId is undefined (enforcing model selection before launch), or modify
the activeProvider selector itself to return undefined instead of a valid
context object when provider.selection.modelId is not set, ensuring downstream
code receives undefined rather than a context with an empty model string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant